package org.openea.eap.module.obpm.service.eap;

import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.sec.etech.security.service.JwtUtil;
import com.xingyuv.captcha.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.openea.eap.framework.common.enums.UserTypeEnum;
import org.openea.eap.framework.common.util.date.DateUtils;
import org.openea.eap.framework.common.util.json.JsonUtils;
import org.openea.eap.framework.security.core.LoginUser;
import org.openea.eap.framework.tenant.core.context.TenantContextHolder;
import org.openea.eap.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import org.openea.eap.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
import org.openea.eap.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO;
import org.openea.eap.module.system.dal.dataobject.user.AdminUserDO;
import org.openea.eap.module.system.dal.mysql.oauth2.OAuth2AccessTokenMapper;
import org.openea.eap.module.system.dal.mysql.oauth2.OAuth2RefreshTokenMapper;
import org.openea.eap.module.system.dal.redis.oauth2.OAuth2AccessTokenRedisDAO;
import org.openea.eap.module.system.enums.oauth2.OAuth2ClientConstants;
import org.openea.eap.module.system.service.oauth2.OAuth2ClientService;
import org.openea.eap.module.system.service.oauth2.OAuth2TokenServiceImpl;
import org.openea.eap.module.system.service.user.AdminUserService;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.TimeUnit;

import static org.openea.eap.module.system.dal.redis.RedisKeyConstants.OAUTH2_ACCESS_TOKEN;

@Service
@Primary
@Slf4j
public class ObmpOAuth2TokenServiceImpl extends OAuth2TokenServiceImpl {

    private final Map<String, String> jwtTokenMap = new Hashtable<String, String>();

    @Resource
    protected AdminUserService userService;

    @Resource
    private OAuth2ClientService oauth2ClientService;


    @Resource
    private OAuth2AccessTokenMapper oauth2AccessTokenMapper;
    @Resource
    private OAuth2RefreshTokenMapper oauth2RefreshTokenMapper;

    @Resource
    private OAuth2AccessTokenRedisDAO oauth2AccessTokenRedisDAO;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    @Transactional
    public OAuth2AccessTokenDO createAccessToken(Long userId, String userKey, Integer userType, String clientId, List<String> scopes) {
        OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
        // 增加冷却机制防止前端异常频繁刷新
//        OAuth2AccessTokenDO accessTokenDO = loadAccessTokenByUser(userKey);
//        if(accessTokenDO!=null){
//            return accessTokenDO;
//        }
        // 创建刷新令牌
        OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userKey, userType, clientDO, scopes);
        // 创建访问令牌
        return createOAuth2AccessToken(refreshTokenDO, clientDO);
    }

    @Transactional
    public OAuth2AccessTokenDO createAccessToken(Long userId, String userKey, Integer userType, String clientId, List<String> scopes, String jwtToken) {
        OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
        // 增加冷却机制防止前端异常频繁刷新
//        OAuth2AccessTokenDO accessTokenDO = loadAccessTokenByUser(userKey);
//        if(accessTokenDO!=null){
//            return accessTokenDO;
//        }
        // 创建刷新令牌
        OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userKey, userType, clientDO, scopes);
        // 创建访问令牌
        return createOAuth2AccessToken(refreshTokenDO, clientDO, jwtToken);
    }


    private static String generateJwtToken(String account, String refreshToken, int accessTokenValiditySeconds) {
        // 避免同一用户同时颁发token出现重复jwtToken
        String audience = "pc_"+refreshToken.substring(refreshToken.length()-4);
        return JwtUtil.generateToken(account, audience, accessTokenValiditySeconds);
    }

    private static String generateRefreshToken() {
        return IdUtil.fastSimpleUUID();
    }

    private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) {
        return createOAuth2AccessToken(refreshTokenDO, clientDO, null);
    }
    private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO, String jwtToken) {
        if(StringUtils.isEmpty(jwtToken)){
            jwtToken = generateJwtToken(refreshTokenDO.getUserKey(), refreshTokenDO.getRefreshToken(), clientDO.getAccessTokenValiditySeconds()+3600);
        }
        OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(jwtToken)
                .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType())
                .setUserKey(refreshTokenDO.getUserKey())
                .setUserInfo(buildUserInfo(refreshTokenDO.getUserId(), refreshTokenDO.getUserType()))
                .setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes())
                .setRefreshToken(refreshTokenDO.getRefreshToken())
                .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds()));
        accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号，避免缓存到 Redis 的时候，无对应的租户编号

        // 先按用户存冷却记录
//        setAccessTokenByUser(refreshTokenDO.getUserKey(), accessTokenDO);

        oauth2AccessTokenMapper.insert(accessTokenDO);
        // 记录到 Redis 中
        oauth2AccessTokenRedisDAO.set(accessTokenDO);
        return accessTokenDO;
    }

    private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, String userKey, Integer userType, OAuth2ClientDO clientDO, List<String> scopes) {
        OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken())
                .setUserId(userId).setUserKey(userKey).setUserType(userType)
                .setClientId(clientDO.getClientId()).setScopes(scopes)
                .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getRefreshTokenValiditySeconds()));
        oauth2RefreshTokenMapper.insert(refreshToken);
        return refreshToken;
    }

    /**
     * 加载用户信息，方便 {@link org.openea.eap.framework.security.core.LoginUser} 获取到昵称、部门等信息
     *
     * @param userId 用户编号
     * @param userType 用户类型
     * @return 用户信息
     */
    private Map<String, String> buildUserInfo(Long userId, Integer userType) {
        if (userType.equals(UserTypeEnum.ADMIN.getValue())) {
            AdminUserDO user = userService.getUser(userId);
            return MapUtil.builder(LoginUser.INFO_KEY_NICKNAME, user.getNickname()).build();
        } else if (userType.equals(UserTypeEnum.MEMBER.getValue())) {
            // 注意：目前 Member 暂时不读取，可以按需实现
            return Collections.emptyMap();
        }
        return null;
    }

    private OAuth2AccessTokenDO createTempAccessToken(String accessToken, Long userId, String userKey, int accessTokenValiditySeconds){
        // 构造一个token
        OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO();
        accessTokenDO.setAccessToken(accessToken);
        accessTokenDO.setUserId(userId);
        accessTokenDO.setUserKey(userKey);
        accessTokenDO.setUserType(UserTypeEnum.ADMIN.getValue());
        accessTokenDO.setClientId(OAuth2ClientConstants.CLIENT_ID_DEFAULT);
        accessTokenDO.setExpiresTime(LocalDateTime.now().plusSeconds(accessTokenValiditySeconds));
        return accessTokenDO;
    }


    @Override
    public OAuth2AccessTokenDO getAccessToken(String accessToken) {
        if(accessToken==null){
            return null;
        }
        // 先从父类获取，
//        OAuth2AccessTokenDO accessTokenDO = super.getAccessToken(accessToken);
        // 优先从 Redis 中获取
        OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenRedisDAO.get(accessToken);
        if (accessTokenDO != null) {
            return accessTokenDO;
        }

        // 获取不到，从 MySQL 中获取
        try{

            // fix 错误数据 accessToken多条数据
            //accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken);
            List<OAuth2AccessTokenDO> accessTokenList = oauth2AccessTokenMapper.selectList(new QueryWrapper<OAuth2AccessTokenDO>().eq("access_token", accessToken).orderByDesc("create_time"));
            if(accessTokenList!=null && accessTokenList.size()>0){
                accessTokenDO = accessTokenList.get(0);
            }

            // 如果在 MySQL 存在，则往 Redis 中写入
            if (accessTokenDO != null && !DateUtils.isExpired(accessTokenDO.getExpiresTime())) {
                oauth2AccessTokenRedisDAO.set(accessTokenDO);
            }
            if (accessTokenDO != null) {
                return accessTokenDO;
            }

        }catch(Exception e){
            log.warn("selectByAccessToken {}", accessToken, e);
        }

        // 针对etech定制，解析jwt token后构造后将token加入redis(此类特殊token有效期2小时)
        if(accessToken.startsWith("eyJh")){
            try{
                String username = JwtUtil.getValidSubjectFromRedisToken(accessToken);
                if(StringUtils.isEmpty(username)){
                    return null;
                }
                // 校验账号是否存在
                AdminUserDO user = userService.getUserByUsername(username);
                if(user!=null){
                    // 构造一个token
                    int accessTokenValiditySeconds = 60 * 60 * 2; // 有效期2小时
                    accessTokenDO = createTempAccessToken(accessToken, user.getId(), username, accessTokenValiditySeconds);
                    // 记录到 Redis 中
                    oauth2AccessTokenRedisDAO.set(accessTokenDO);
                    log.info("createTempAccessToken, save token to redis: user={}, jwtToken={}", username, accessToken);
                }
                return accessTokenDO;
            }catch(Throwable t){
                log.warn(t.getMessage()+" token="+accessToken, t);
            }
        }
        return accessTokenDO;
    }



    @Override
    public OAuth2AccessTokenDO removeAccessToken(String accessToken) {
        // 删除访问令牌
        List<OAuth2AccessTokenDO> accessTokenList = oauth2AccessTokenMapper.selectList(new QueryWrapper<OAuth2AccessTokenDO>().eq("access_token", accessToken).orderByDesc("create_time"));
        if(accessTokenList!=null && accessTokenList.size()>0){
            OAuth2AccessTokenDO accessTokenDO2 = null;
            for(OAuth2AccessTokenDO accessTokenDO:accessTokenList){
                accessTokenDO2 = accessTokenDO;
                try{
                    oauth2AccessTokenMapper.deleteById(accessTokenDO.getId());
                    oauth2RefreshTokenMapper.deleteByRefreshToken(accessTokenDO.getRefreshToken());
                }catch (Exception e) {
                    log.warn("removeAccessToken "+accessToken, e);
                }
            }
            oauth2AccessTokenRedisDAO.delete(accessToken);
            return accessTokenDO2;
        }
        return null;
    }

    private OAuth2AccessTokenDO loadAccessTokenByUser(String userKey) {
        // 从redis中获取
        String redisKey = userTokenFormatKey(userKey);
        return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), OAuth2AccessTokenDO.class);
    }

    public void setAccessTokenByUser(String userKey, OAuth2AccessTokenDO accessTokenDO) {
        String redisKey = userTokenFormatKey(userKey);
        // 清理多余字段，避免缓存
        accessTokenDO.setUpdater(null).setUpdateTime(null).setCreateTime(null).setCreator(null).setDeleted(null);
        long time = 5; //冷却时间，单位秒
        stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(accessTokenDO), time, TimeUnit.SECONDS);
    }

    private static String userTokenFormatKey(String userKey) {
        return String.format("oauth2_access_token:%s", userKey);
    }


    private OAuth2AccessTokenDO copyOAuth2CustToken(OAuth2AccessTokenDO accessTokenDO, String newToken, Integer accessTokenValiditySeconds) {
        OAuth2AccessTokenDO custTokenDO = new OAuth2AccessTokenDO().setAccessToken(newToken)
                .setUserId(accessTokenDO.getUserId()).setUserType(accessTokenDO.getUserType())
                .setUserKey(accessTokenDO.getUserKey())
                .setUserInfo(accessTokenDO.getUserInfo())
                .setClientId(accessTokenDO.getClientId()).setScopes(accessTokenDO.getScopes())
                .setRefreshToken(accessTokenDO.getRefreshToken())
                .setExpiresTime(LocalDateTime.now().plusSeconds(accessTokenValiditySeconds));
        custTokenDO.setTenantId(TenantContextHolder.getTenantId());
        oauth2AccessTokenMapper.insert(custTokenDO);
        // 记录到 Redis 中
        oauth2AccessTokenRedisDAO.set(custTokenDO);
        return custTokenDO;
    }

}
